iT邦幫忙

2022 iThome 鐵人賽

DAY 13
0
Software Development

你知道Go是什麼嗎?系列 第 13

Day13 - Channel - Golang

  • 分享至 

  • xImage
  •  

耶,第十三天,昨天講了goroutine,今天就來講channel吧!你說這兩個有什麼關聯?我一開始也想了很久,希望今天的文章可以解釋到

Channel

  • 一種資料型態,類似queue
  • 可以設定大小放入資料,可使用<-將資料放入與取出
  • 內建等待功能,要取或推資料時若channel內沒有東西,程式會等待另一端操作完

建立Channel

ch := make(chan int , n) // 建立 int 型別長度為 n 的 Channel

輸入 / 輸出

使用 <- 來操作資料,想像channel為管道,右進左出,ch<-data為將data放入,<-ch為取出資料

ch := make(chan int , 1)
ch <- 5
fmt.Println(<-ch) // 5

可利用len()cap()來確認裡面有多少筆資料與容量,

ch := make(chan int , 1)   // (len(ch),cap(ch)) = 0,1
ch <- 5                    // (len(ch),cap(ch)) = 1,1
fmt.Println(<-ch) // 5     // (len(ch),cap(ch)) = 0,1

可以看出5被放入時len=1,取出後len=0

Channel阻塞

Channel在推入與拉出資料時都有可能發生阻塞問題

  • 當資料推入長度1的channel時,在這筆資料拉出之前,其他的ch<-data會被卡住
  • ch內沒資料時,<-ch會卡住,直到channel內有資料時才執行

推入資料的等待情境

func main() {
    ch := make(chan int, 1)
    ch <- 1
    ch <- 2
    fmt.Println("Hi")
}

卡在 ch <- 2 這行,程式直接暫停

拉出資料的等待情境

func main() {
    ch := make(chan int)
    fmt.Println(<-ch)
    fmt.Println("Hi")
}

長度為0,資料放不進去,<-ch這邊卡住,又暫停了

使用時機

好啦,那這個像水管一樣的資料型態,什麼時候可以用上?從昨天的goroutineRace Condition開始講好了。

Race Condition

競爭條件?這是什麼東西,我們先看一下下面的例子

func main() {
    total := 0
	
    for i := 0; i < 1000; i++ {
        go func(){
    		total++
    	}()
    }

    time.Sleep(time.Second)
    fmt.Println(total)
}

output:

982

輸出怎麼不是1000?其實也不一定是982,每次執行數字都會浮動。那是因為goroutine在為total進行++時,說不定還沒處理完並存成新的total,原先的goroutine就又進入了迴圈並取得了total去做運算,這樣雖然跑了兩次++,但其實total只加了一次,這是多執行續時一定會遇上的問題

帶數字舉例。total此時是50

  1. 進入go func去做total=total+1
  2. main又進了迴圈一次,跑go func
  3. 由於第一步尚未運算完,因此goroutine2取得的total還是50
  4. goroutine1goroutine2同時運算完,兩個都回傳total=51

多執行續偶爾會遇到這種變數問題。這就是為什麼上面的答案會與預期有落差。

這在作業系統上稱做Race Condition,有更簡單的例子啦,只是我想結合goroutine一起講,可以查一下其他Race Condition的例子。

為變數上鎖

有種做法是為變數上鎖,當有人在使用時就禁止別人使用,Go可以使用互斥鎖來實做這個方法。

type SafeNumber struct {
    v   int
    mux sync.Mutex // 互斥鎖
}

func main() {
    total := SafeNumber{v:0}    
    
    for i := 0; i < 1000; i++ {
        go func(){
    		total.mux.Lock()
    		total.v++
    		total.mux.Unlock()
    	}()
    }
    time.Sleep(time.Second)
    total.mux.Lock()
    fmt.Println(total.v)
    total.mux.Unlock()
}

output:

1000

  • 建立SafeNumber結構,內有sync.Mutex互斥鎖
  • 在每次使用SafeNumber.v的前後都使用LockUnlock上鎖與開鎖,確保同一時間只有一執行續在操作變數。

使用Channel

Go提供了channel這個資料型態來完成操作共同變數的功能,Go官方文章說明了這點。

同個例子改成使用channel

func main() {
    ch := make(chan int, 1)
    ch <- 0
    for i := 0; i < 1000; i++ {
    	go func() {
    		ch <- (<-ch + 1)
    	}()
    }
    time.Sleep(time.Second)
    fmt.Println(<-ch)
}

output:

1000

每一個go func執行順序如下

  1. <-ch 拉出ch內的資料
  2. (<-ch + 1) 將拉出來的資料+1
  3. ch <- (<-ch + 1) 推回ch

因此就算goroutine2趕上了goroutine1,只要goroutine1還沒將資料放回chgoroutine2就會進入等待期,避免搶奪問題發生

Unbuffered、Buffered Channel

Channel又分為這兩種。Buffered,緩衝區,顧名思義就是有沒有緩衝區的channel而已。在宣告時會一併宣告channel大小,若沒輸入預設長度為0。

  • Unbuffered Channel: 在使用上若有東西塞入channel,一定要靠另一個goroutine將東西拉出去後才有辦法繼續執行,否則會卡住
  • Buffered Channel: 可將東西儲存至容量上限,等待另一端將資料取出

使用select

goroutine進入等待期時使用者是不清楚的,因此可以使用select讓程式在進入等待期時也能有一點輸出。

ch := make(chan string)
    go func() {
    	time.Sleep(time.Second) //模擬費時運算
    	ch <- "FINISH"
    }()

for {
    select {
    case (<-ch): // Channel 中有資料執行此區域
        fmt.Println("main完成")
    return
    default: // Channel 阻塞的話執行此區域
        fmt.Println("waiting...")
        time.Sleep(500 * time.Millisecond)
    }
}

output:

waiting...
waiting...
waiting...
main完成

這種方式就能在等待期也輸出一點資料


完成啦,又完成了一篇耗時的文章,跟昨天一樣卡了很久看不懂到底要channel幹嘛,明明原本的資料型態就夠了幹嘛還多學一個。不過感覺與goroutine一樣以後在寫系統相關程式或微服務時應該都會用到,先學起來以後用吧

參考資料

Go 的並發:Goroutine 與 Channel 介紹
https://peterhpchen.github.io/2020/03/08/goroutine-and-channel.html

olang 教學系列 - 何謂 Channel? 先從宣告 Channel 開始學起!
https://blog.kennycoder.io/2020/12/23/Golang%E6%95%99%E5%AD%B8%E7%B3%BB%E5%88%97-%E4%BD%95%E8%AC%82Channel-%E5%85%88%E5%BE%9E%E5%AE%A3%E5%91%8AChannel%E9%96%8B%E5%A7%8B%E5%AD%B8%E8%B5%B7/


上一篇
Day12 - Goroutine - Golang
下一篇
Day14-Web-Golang
系列文
你知道Go是什麼嗎?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言